require 'geo_ruby/simple_features/geometry'

module GeoRuby
  module SimpleFeatures
    # Represents a line string as an array of points (see Point).
    class LineString < Geometry
      # the list of points forming the line string
      attr_reader :points

      def initialize(srid = DEFAULT_SRID, with_z = false, with_m = false)
        super(srid, with_z, with_m)
        @points = []
      end

      # Delegate the unknown methods to the points array
      def method_missing(method_name, *args, &b)
        @points.send(method_name, *args, &b)
      end

      # tests if the line string is closed
      def closed?
        # a bit naive...
        @points.first == @points.last
      end

      def clockwise?
        tuples = @points.zip(
          @points[1..-1] + [@points[0]],
          @points[2..-1] + [@points[0], @points[1]])
        tuples.map! { |a, b, c| b.x * (c.y - a.y)  }
        sum = tuples.reduce(0.0) { |a, e| a + e }
        sum < 0.0
      end

      # Bounding box in 2D/3D. Returns an array of 2 points
      def bounding_box
        max_x = max_y = -Float::MAX
        min_x = min_y = Float::MAX
        if with_z
          max_z, min_z = -Float::MAX, Float::MAX
          each do |point|
            max_y = point.y if point.y > max_y
            min_y = point.y if point.y < min_y
            max_x = point.x if point.x > max_x
            min_x = point.x if point.x < min_x
            max_z = point.z if point.z > max_z
            min_z = point.z if point.z < min_z
          end
          [Point.from_x_y_z(min_x, min_y, min_z), Point.from_x_y_z(max_x, max_y, max_z)]
        else
          each do |point|
            max_y = point.y if point.y > max_y
            min_y = point.y if point.y < min_y
            max_x = point.x if point.x > max_x
            min_x = point.x if point.x < min_x
          end
          [Point.from_x_y(min_x, min_y), Point.from_x_y(max_x, max_y)]
        end
      end

      def m_range
        if with_m
          max_m, min_m = -Float::MAX, Float::MAX
          each do |point|
            max_m = point.m if point.m.to_f > max_m
            min_m = point.m if point.m.to_f < min_m
          end
          [min_m, max_m]
        else
          [0, 0]
        end
      end

      # call to native Geo intersect, return true or false
      def intersects?(_other_line_string)
      end

      def spherical_distance
        total = 0
        @points.each_with_index do |p, i|
          total += p.spherical_distance(@points[i + 1]) if @points[i + 1]
        end
        total
      end

      def euclidian_distance
        total = 0
        @points.each_with_index do |p, i|
          total += p.euclidian_distance(@points[i + 1]) if @points[i + 1]
        end
        total
      end

      # Tests the equality of line strings
      def ==(other)
        if other.class != self.class ||
           other.length != length
          false
        else
          index = 0
          while index < length
            return false if self[index] != other[index]
            index += 1
          end
          true
        end
      end

      # Binary representation of a line string
      def binary_representation(allow_z = true, allow_m = true) #:nodoc:
        rep = [length].pack('V')
        each { |point| rep << point.binary_representation(allow_z, allow_m) }
        rep
      end

      # WKB geometry type
      def binary_geometry_type #:nodoc:
        2
      end

      # Text representation of a line string
      def text_representation(allow_z = true, allow_m = true) #:nodoc:
        @points.collect { |point| point.text_representation(allow_z, allow_m) }.join(',')
      end
      # WKT geometry type
      def text_geometry_type #:nodoc:
        'LINESTRING'
      end

      # georss simple representation
      def georss_simple_representation(options) #:nodoc:
        georss_ns = options[:georss_ns] || 'georss'
        geom_attr = options[:geom_attr]
        "<#{georss_ns}:line#{geom_attr}>#{georss_poslist}</#{georss_ns}:line>\n"
      end

      # georss w3c representation : outputs the first point of the line
      def georss_w3cgeo_representation(options) #:nodoc:
        w3cgeo_ns = options[:w3cgeo_ns] || 'geo'
        "<#{w3cgeo_ns}:lat>#{self[0].y}</#{w3cgeo_ns}:lat>\n"\
        "<#{w3cgeo_ns}:long>#{self[0].x}</#{w3cgeo_ns}:long>\n"
      end

      # georss gml representation
      def georss_gml_representation(options) #:nodoc:
        georss_ns = options[:georss_ns] || 'georss'
        gml_ns = options[:gml_ns] || 'gml'

        "<#{georss_ns}:where>\n<#{gml_ns}:LineString>\n<#{gml_ns}:posList>\n" \
        "#{georss_poslist}\n</#{gml_ns}:posList>\n</#{gml_ns}:LineString>\n" \
        "</#{georss_ns}:where>\n"
      end

      def georss_poslist #:nodoc:
        map { |point| "#{point.y} #{point.x}" }.join(' ')
      end

      # outputs the geometry in kml format : options are <tt>:id</tt>,
      # <tt>:tesselate</tt>, <tt>:extrude</tt>, <tt>:altitude_mode</tt>.
      # If the altitude_mode option is not present, the Z (if present) will not
      # be output (since it won't be used by GE: clampToGround is the default)
      def kml_representation(options = {}) #:nodoc:
        result = "<LineString#{options[:id_attr]}>\n"
        result += options[:geom_data] if options[:geom_data]
        result += '<coordinates>'
        result += kml_poslist(options)
        result += "</coordinates>\n"
        result + "</LineString>\n"
      end

      def kml_poslist(options) #:nodoc:
        pos_list = \
        if options[:allow_z]
          map { |point| "#{point.x},#{point.y},#{options[:fixed_z] || point.z || 0}" }
        else
          map { |point| "#{point.x},#{point.y}" }
        end
        pos_list.reverse! if options[:reverse]
        pos_list.join(' ')
      end

      # Simplify linestring (Douglas Peucker Algorithm)
      # http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
      def simplify(epsilon = 1)
        LineString.from_points(do_simplify(@points, epsilon))
      end

      def do_simplify(list, epsilon)
        index = dmax = 0
        2.upto(list.length - 1) do |i|
          d = list[i].orthogonal_distance(list[0], list[-1])
          index, dmax = i, d if d > dmax
        end
        return [list[0], list[-1]] if dmax < epsilon
        res1 = do_simplify(list[0..index], epsilon)
        res2 = do_simplify(list[index..-1], epsilon)
        res1[0..-2] + res2[0..-1]
      end

      def to_coordinates
        points.map(&:to_coordinates)
      end

      def as_json(_options = {})
        { type: 'LineString', coordinates: to_coordinates }
      end

      # Creates a new line string. Accept an array of points as argument
      def self.from_points(points, srid = DEFAULT_SRID, z = false, m = false)
        line_string = new(srid, z, m)
        line_string.concat(points)
        line_string
      end

      # Creates a new line string. Accept a sequence of points as argument:
      #  ((x,y)...(x,y))
      def self.from_coordinates(points, srid = DEFAULT_SRID, z = false, m = false)
        line_string = new(srid, z, m)
        line_string.concat(points.map { |p| Point.from_coordinates(p, srid, z, m) })
        line_string
      end
    end # LineString
  end # SimpleFeatures
end # GeoRuby
